package com.chenqq.camerademo.camera.es;


import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.opengl.GLES20;
import android.util.Log;
import android.util.SparseArray;
import android.view.Surface;

import com.chenqq.camerademo.camera.utils.BuildCheck;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import androidx.annotation.Nullable;


public class RendererHolder
        implements IRendererHolder {
    private static final String TAG = RendererHolder.class.getSimpleName();

    private final Object mSync = new Object();


    private final RenderHolderCallback mCallback;


    private volatile boolean isRunning;


    private File mCaptureFile;


    private final RendererTask mRendererTask;


    private static final int REQUEST_DRAW = 1;


    private static final int REQUEST_UPDATE_SIZE = 2;


    private static final int REQUEST_ADD_SURFACE = 3;


    private static final int REQUEST_REMOVE_SURFACE = 4;

    private static final int REQUEST_RECREATE_MASTER_SURFACE = 5;

    private static final int REQUEST_MIRROR = 6;

    private final Runnable mCaptureTask;


    public boolean isRunning() {
        return this.isRunning;
    }


    public void release() {

        this.mRendererTask.release();

        synchronized (this.mSync) {

            this.isRunning = false;

            this.mSync.notifyAll();
        }
    }


    public Surface getSurface() {
        return this.mRendererTask.getSurface();
    }


    public SurfaceTexture getSurfaceTexture() {
        return this.mRendererTask.getSurfaceTexture();
    }


    public void reset() {
        this.mRendererTask.checkMasterSurface();
    }


    public void resize(int width, int height) {
        this.mRendererTask.resize(width, height);
    }


    public void setMirror(int mirror) {
        this.mRendererTask.mirror(mirror % 4);
    }


    public int getMirror() {
        return this.mRendererTask.mirror();
    }


    public void addSurface(int id, Object surface, boolean isRecordable) {
        this.mRendererTask.addSurface(id, surface);
    }


    public void addSurface(int id, Object surface, boolean isRecordable, int maxFps) {
        this.mRendererTask.addSurface(id, surface, maxFps);
    }


    public void removeSurface(int id) {
        this.mRendererTask.removeSurface(id);
    }


    public boolean isEnabled(int id) {
        return this.mRendererTask.isEnabled(id);
    }


    public void setEnabled(int id, boolean enable) {
        this.mRendererTask.setEnabled(id, enable);
    }


    public void requestFrame() {

        this.mRendererTask.removeRequest(1);

        this.mRendererTask.offer(1);
    }


    public int getCount() {
        return this.mRendererTask.getCount();
    }


    public void captureStillAsync(String path) {

        File file = new File(path);

        synchronized (this.mSync) {

            this.mCaptureFile = file;

            this.mSync.notifyAll();
        }
    }


    public void captureStill(String path) {

        File file = new File(path);

        synchronized (this.mSync) {

            this.mCaptureFile = file;

            this.mSync.notifyAll();

            try {

                this.mSync.wait();

            } catch (InterruptedException ignored) {
            }
        }
    }


    private static final class RendererTask
            extends EglTask {
        private final Object mClientSync = new Object();
        private final SparseArray<RendererSurfaceRec> mClients = new SparseArray();
        private final RendererHolder mParent;
        private GLDrawer2D mDrawer;
        private int mTexId;
        private SurfaceTexture mMasterTexture;
        final float[] mTexMatrix = new float[16];
        private Surface mMasterSurface;
        private int mVideoWidth;
        private int mVideoHeight;
        private int mMirror = 0;
        private final SurfaceTexture.OnFrameAvailableListener mOnFrameAvailableListener;

        public RendererTask(RendererHolder parent, int width, int height) {

            super(3, null, 2);


            this.mOnFrameAvailableListener = new SurfaceTexture.OnFrameAvailableListener() {
                public void onFrameAvailable(SurfaceTexture surfaceTexture) {

                    RendererTask.this.offer(1);
                }
            };
            this.mParent = parent;
            this.mVideoWidth = width;
            this.mVideoHeight = height;
        }

        protected void onStart() {
            this.mDrawer = new GLDrawer2D(true);
            handleReCreateMasterSurface();
            synchronized (this.mParent.mSync) {
                this.mParent.isRunning = true;
                this.mParent.mSync.notifyAll();
            }
        }

        protected void onStop() {
            synchronized (this.mParent.mSync) {
                this.mParent.isRunning = false;
                this.mParent.mSync.notifyAll();
            }
            makeCurrent();
            if (this.mDrawer != null) {
                this.mDrawer.release();
                this.mDrawer = null;
            }
            handleReleaseMasterSurface();
            handleRemoveAll();
        }

        protected boolean onError(Exception e) {
            return false;
        }

        protected Object processRequest(int request, int arg1, int arg2, Object obj) {
            switch (request) {
                case 1:
                    handleDraw();
                    break;
                case 2:
                    handleResize(arg1, arg2);
                    break;
                case 3:
                    handleAddSurface(arg1, obj, arg2);
                    break;
                case 4:
                    handleRemoveSurface(arg1);
                    break;
                case 5:
                    handleReCreateMasterSurface();
                    break;
                case 6:
                    handleMirror(arg1);
                    break;
            }
            return null;
        }

        public Surface getSurface() {
            checkMasterSurface();
            return this.mMasterSurface;
        }

        public SurfaceTexture getSurfaceTexture() {
            checkMasterSurface();
            return this.mMasterTexture;
        }

        public void addSurface(int id, Object surface) {
            addSurface(id, surface, -1);
        }

        public void addSurface(int id, Object surface, int maxFps) {
            checkFinished();
            if (!(surface instanceof SurfaceTexture) && !(surface instanceof Surface) && !(surface instanceof android.view.SurfaceHolder))
                throw new IllegalArgumentException("Surface should be one of Surface, SurfaceTexture or SurfaceHolder");
            synchronized (this.mClientSync) {
                if (this.mClients.get(id) == null) while (isRunning()) {
                    if (offer(3, id, maxFps, surface)) try {
                        this.mClientSync.wait();
                        break;
                    } catch (InterruptedException interruptedException) {
                        break;
                    }
                    try {
                        this.mClientSync.wait(10L);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            }
        }

        public void removeSurface(int id) {
            synchronized (this.mClientSync) {
                if (this.mClients.get(id) != null) while (isRunning()) {
                    if (offer(4, id)) try {
                        this.mClientSync.wait();
                        break;
                    } catch (InterruptedException interruptedException) {
                        break;
                    }
                    try {
                        this.mClientSync.wait(10L);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            }
        }

        public boolean isEnabled(int id) {
            synchronized (this.mClientSync) {
                RendererSurfaceRec rec = (RendererSurfaceRec) this.mClients.get(id);
                return (rec != null && rec.isEnabled());
            }
        }

        public void setEnabled(int id, boolean enable) {
            synchronized (this.mClientSync) {
                RendererSurfaceRec rec = (RendererSurfaceRec) this.mClients.get(id);
                if (rec != null) rec.setEnabled(enable);
            }
        }

        public int getCount() {
            synchronized (this.mClientSync) {
                return this.mClients.size();
            }
        }

        public void resize(int width, int height) {
            checkFinished();
            if (this.mVideoWidth != width || this.mVideoHeight != height) offer(2, width, height);
        }

        public void mirror(int mirror) {
            checkFinished();
            if (this.mMirror != mirror) offer(6, mirror);
        }

        public int mirror() {
            return this.mMirror;
        }

        public void checkMasterSurface() {
            checkFinished();
            if (this.mMasterSurface == null || !this.mMasterSurface.isValid()) {
                Log.d(TAG, "checkMasterSurface:invalid master surface");
                offerAndWait(5, 0, 0, null);
            }
        }

        private void checkFinished() {
            if (isFinished()) throw new RuntimeException("already finished");
        }

        private void handleDraw() {
            if (this.mMasterSurface == null || !this.mMasterSurface.isValid()) {
                Log.w(TAG, "checkMasterSurface:invalid master surface");
                offer(5);
                return;
            }
            try {
                makeCurrent();
                this.mMasterTexture.updateTexImage();
                this.mMasterTexture.getTransformMatrix(this.mTexMatrix);
            } catch (Exception e) {
                Log.w(TAG, "draw:thread id =" + Thread.currentThread().getId(), e);
                offer(5);
                return;
            }
            synchronized (this.mParent.mCaptureTask) {
                this.mParent.mCaptureTask.notify();
            }
            synchronized (this.mClientSync) {
                int n = this.mClients.size();
                for (int i = n - 1; i >= 0; i--) {
                    RendererSurfaceRec client = (RendererSurfaceRec) this.mClients.valueAt(i);
                    if (client != null && client.canDraw()) try {
                        client.draw(this.mDrawer, this.mTexId, this.mTexMatrix);
                    } catch (Exception e) {
                        this.mClients.removeAt(i);
                        client.release();
                    }
                }
            }
            if (this.mParent.mCallback != null) try {
                this.mParent.mCallback.onFrameAvailable();
            } catch (Exception ignored) {
            }
            GLES20.glClear(16384);
            GLES20.glFlush();
        }

        private void handleAddSurface(int id, Object surface, int maxFps) {
            checkSurface();
            synchronized (this.mClientSync) {
                RendererSurfaceRec client = (RendererSurfaceRec) this.mClients.get(id);
                if (client == null) {
                    try {
                        client = RendererSurfaceRec.newInstance(getEgl(), surface, maxFps);
                        setMirror(client, this.mMirror);
                        this.mClients.append(id, client);
                    } catch (Exception e) {
                        Log.w(TAG, "invalid surface: surface=" + surface, e);
                    }
                } else {
                    Log.w(TAG, "surface is already added: id=" + id);
                }
                this.mClientSync.notifyAll();
            }
        }

        private void handleRemoveSurface(int id) {
            synchronized (this.mClientSync) {
                RendererSurfaceRec client = (RendererSurfaceRec) this.mClients.get(id);
                if (client != null) {
                    this.mClients.remove(id);
                    client.release();
                }
                checkSurface();
                this.mClientSync.notifyAll();
            }
        }

        private void handleRemoveAll() {
            synchronized (this.mClientSync) {
                int n = this.mClients.size();
                for (int i = 0; i < n; i++) {
                    RendererSurfaceRec client = (RendererSurfaceRec) this.mClients.valueAt(i);
                    if (client != null) {
                        makeCurrent();
                        client.release();
                    }
                }
                this.mClients.clear();
            }
        }

        private void checkSurface() {
            synchronized (this.mClientSync) {
                int n = this.mClients.size();
                for (int i = 0; i < n; i++) {
                    RendererSurfaceRec client = (RendererSurfaceRec) this.mClients.valueAt(i);
                    if (client != null && !client.isValid()) {
                        int id = this.mClients.keyAt(i);
                        ((RendererSurfaceRec) this.mClients.valueAt(i)).release();
                        this.mClients.remove(id);
                    }
                }
            }
        }

        @SuppressLint({"NewApi"})
        private void handleReCreateMasterSurface() {
            makeCurrent();
            handleReleaseMasterSurface();
            makeCurrent();
            this.mTexId = GLHelper.initTex(36197, 9728);
            this.mMasterTexture = new SurfaceTexture(this.mTexId);
            this.mMasterSurface = new Surface(this.mMasterTexture);
            if (BuildCheck.isAndroid4_1())
                this.mMasterTexture.setDefaultBufferSize(this.mVideoWidth, this.mVideoHeight);
            this.mMasterTexture.setOnFrameAvailableListener(this.mOnFrameAvailableListener);
            try {
                if (this.mParent.mCallback != null)
                    this.mParent.mCallback.onCreate(this.mMasterSurface);
            } catch (Exception e) {
                Log.w(TAG, e);
            }
        }

        private void handleReleaseMasterSurface() {
            try {
                if (this.mParent.mCallback != null) this.mParent.mCallback.onDestroy();
            } catch (Exception e) {
                Log.w(TAG, e);
            }
            this.mMasterSurface = null;
            if (this.mMasterTexture != null) {
                this.mMasterTexture.release();
                this.mMasterTexture = null;
            }
            if (this.mTexId != 0) {
                GLHelper.deleteTex(this.mTexId);
                this.mTexId = 0;
            }
        }

        @SuppressLint({"NewApi"})
        private void handleResize(int width, int height) {
            this.mVideoWidth = width;
            this.mVideoHeight = height;
            if (BuildCheck.isAndroid4_1())
                this.mMasterTexture.setDefaultBufferSize(this.mVideoWidth, this.mVideoHeight);
        }

        private void handleMirror(int mirror) {
            this.mMirror = mirror;
            synchronized (this.mClientSync) {
                int n = this.mClients.size();
                for (int i = 0; i < n; i++) {
                    RendererSurfaceRec client = (RendererSurfaceRec) this.mClients.valueAt(i);
                    if (client != null) setMirror(client, mirror);
                }
            }
        }

        private void setMirror(RendererSurfaceRec client, int mirror) {
            float[] mvp = client.mMvpMatrix;
            switch (mirror) {
                case 0:
                    mvp[0] = Math.abs(mvp[0]);
                    mvp[5] = Math.abs(mvp[5]);
                    break;
                case 1:
                    mvp[0] = -Math.abs(mvp[0]);
                    mvp[5] = Math.abs(mvp[5]);
                    break;
                case 2:
                    mvp[0] = Math.abs(mvp[0]);
                    mvp[5] = -Math.abs(mvp[5]);
                    break;
                case 3:
                    mvp[0] = -Math.abs(mvp[0]);
                    mvp[5] = -Math.abs(mvp[5]);
                    break;
            }
        }

    }

    public RendererHolder(int width, int height, @Nullable RenderHolderCallback callback) {
        this.mCaptureTask = new Runnable() {
            EGLBase eglBase;

            EGLBase.IEglSurface captureSurface;
            GLDrawer2D drawer;

            public void run() {

                synchronized (RendererHolder.this.mSync) {


                    if (!RendererHolder.this.isRunning) {
                        try {

                            RendererHolder.this.mSync.wait();

                        } catch (InterruptedException interruptedException) {
                        }
                    }
                }

                init();

                if (this.eglBase.getGlVersion() > 2) {

                    captureLoopGLES3();
                } else {

                    captureLoopGLES2();
                }


                release();
            }


            private final void init() {

                this.eglBase = EGLBase.createFrom(3, RendererHolder.this.mRendererTask.getContext(), false, 0, false);

                this.captureSurface = this.eglBase.createOffscreen(RendererHolder.this.mRendererTask.mVideoWidth, RendererHolder.this.mRendererTask.mVideoHeight);

                this.drawer = new GLDrawer2D(true);

                this.drawer.getMvpMatrix()[5] = this.drawer.getMvpMatrix()[5] * -1.0F;
            }

            private final void captureLoopGLES2() {

                int width = -1, height = -1;

                ByteBuffer buf = null;

                File captureFile = null;


                while (RendererHolder.this.isRunning) {

                    synchronized (RendererHolder.this.mSync) {

                        if (captureFile == null) {

                            if (RendererHolder.this.mCaptureFile == null) {
                                try {

                                    RendererHolder.this.mSync.wait();

                                } catch (InterruptedException e) {
                                    break;
                                }
                            }

                            if (RendererHolder.this.mCaptureFile != null) {


                                captureFile = RendererHolder.this.mCaptureFile;

                                RendererHolder.this.mCaptureFile = null;
                            }
                            continue;
                        }

                        if ((((buf == null) ? 1 : 0) | ((width != RendererHolder.this.mRendererTask.mVideoWidth) ? 1 : 0)) != 0 || height != RendererHolder.this.mRendererTask.mVideoHeight) {

                            width = RendererHolder.this.mRendererTask.mVideoWidth;

                            height = RendererHolder.this.mRendererTask.mVideoHeight;

                            buf = ByteBuffer.allocateDirect(width * height * 4);

                            buf.order(ByteOrder.LITTLE_ENDIAN);

                            if (this.captureSurface != null) {

                                this.captureSurface.release();

                                this.captureSurface = null;
                            }

                            this.captureSurface = this.eglBase.createOffscreen(width, height);
                        }

                        if (RendererHolder.this.isRunning) {

                            this.captureSurface.makeCurrent();

                            this.drawer.draw(RendererHolder.this.mRendererTask.mTexId, RendererHolder.this.mRendererTask.mTexMatrix, 0);

                            this.captureSurface.swap();

                            buf.clear();

                            GLES20.glReadPixels(0, 0, width, height, 6408, 5121, buf);


                            Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.PNG;

                            if (captureFile.toString().endsWith(".jpg")) {

                                compressFormat = Bitmap.CompressFormat.JPEG;
                            }

                            BufferedOutputStream os = null;
                            try {
                                try {

                                    os = new BufferedOutputStream(new FileOutputStream(captureFile));

                                    Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

                                    buf.clear();

                                    bmp.copyPixelsFromBuffer(buf);

                                    bmp.compress(compressFormat, 90, os);

                                    bmp.recycle();

                                    os.flush();
                                } finally {

                                    if (os != null) os.close();
                                }

                            } catch (IOException e) {

                                Log.w(TAG, "failed to save file", e);

                            }
                        }

                        captureFile = null;

                        RendererHolder.this.mSync.notifyAll();
                    }
                }
            }


            private final void captureLoopGLES3() {

                int width = -1, height = -1;

                ByteBuffer buf = null;

                File captureFile = null;


                while (RendererHolder.this.isRunning) {

                    synchronized (RendererHolder.this.mSync) {

                        if (captureFile == null) {

                            if (RendererHolder.this.mCaptureFile == null) {
                                try {

                                    RendererHolder.this.mSync.wait();

                                } catch (InterruptedException e) {
                                    break;
                                }
                            }

                            if (RendererHolder.this.mCaptureFile != null) {


                                captureFile = RendererHolder.this.mCaptureFile;

                                RendererHolder.this.mCaptureFile = null;
                            }
                            continue;
                        }

                        if ((((buf == null) ? 1 : 0) | ((width != RendererHolder.this.mRendererTask.mVideoWidth) ? 1 : 0)) != 0 || height != RendererHolder.this.mRendererTask.mVideoHeight) {

                            width = RendererHolder.this.mRendererTask.mVideoWidth;

                            height = RendererHolder.this.mRendererTask.mVideoHeight;

                            buf = ByteBuffer.allocateDirect(width * height * 4);

                            buf.order(ByteOrder.LITTLE_ENDIAN);

                            if (this.captureSurface != null) {

                                this.captureSurface.release();

                                this.captureSurface = null;
                            }

                            this.captureSurface = this.eglBase.createOffscreen(width, height);
                        }

                        if (RendererHolder.this.isRunning) {

                            this.captureSurface.makeCurrent();

                            this.drawer.draw(RendererHolder.this.mRendererTask.mTexId, RendererHolder.this.mRendererTask.mTexMatrix, 0);

                            this.captureSurface.swap();

                            buf.clear();

                            GLES20.glReadPixels(0, 0, width, height, 6408, 5121, buf);


                            Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.PNG;

                            if (captureFile.toString().endsWith(".jpg")) {

                                compressFormat = Bitmap.CompressFormat.JPEG;
                            }

                            BufferedOutputStream os = null;
                            try {
                                try {

                                    os = new BufferedOutputStream(new FileOutputStream(captureFile));

                                    Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

                                    buf.clear();

                                    bmp.copyPixelsFromBuffer(buf);

                                    bmp.compress(compressFormat, 90, os);

                                    bmp.recycle();

                                    os.flush();
                                } finally {

                                    if (os != null) os.close();
                                }

                            } catch (IOException e) {

                                Log.w(TAG, "failed to save file", e);

                            }
                        }


                        captureFile = null;

                        RendererHolder.this.mSync.notifyAll();
                    }
                }
            }

            private final void release() {

                if (this.captureSurface != null) {

                    this.captureSurface.makeCurrent();

                    if (this.drawer != null) {

                        this.drawer.release();
                    }

                    this.captureSurface.release();

                    this.captureSurface = null;
                }

                if (this.drawer != null) {

                    this.drawer.release();

                    this.drawer = null;
                }

                if (this.eglBase != null) {

                    this.eglBase.release();

                    this.eglBase = null;
                }
            }
        };
        this.mCallback = callback;
        this.mRendererTask = new RendererTask(this, width, height);
        (new Thread((Runnable) this.mRendererTask, TAG)).start();
        if (!this.mRendererTask.waitReady())
            throw new RuntimeException("failed to start renderer thread");
        (new Thread(this.mCaptureTask, "CaptureTask")).start();
        synchronized (this.mSync) {
            if (!this.isRunning)
                try {
                    this.mSync.wait();
                } catch (InterruptedException ignored) {
                }
        }
    }

}

